using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using System.Collections.Generic;
using System.Reflection;
using DMTT.Workflow.Activities.Design;
using System.Drawing.Design;

namespace DMTT.Workflow.Activities
{
    [Designer(typeof(MapperActivityDesigner), typeof(IDesigner))]
    [ActivityValidator(typeof(MapperActivityValidator))]
    [ToolboxBitmap(typeof(MapperActivity), MapperConstants.ToolboxIconResource)]
	public class MapperActivity: Activity, IMapProperties
    {
        #region constructors

        public MapperActivity()
		{
            this.SetReadOnlyPropertyValue(InternalMappingsProperty, new MappingCollection());
            this.SetReadOnlyPropertyValue(ParametersProperty, new WorkflowParameterBindingCollection(this));
        }

        public MapperActivity(string name)
            : base(name)
        {
            this.SetReadOnlyPropertyValue(InternalMappingsProperty, new MappingCollection());
            this.SetReadOnlyPropertyValue(ParametersProperty, new WorkflowParameterBindingCollection(this));
        }

        #endregion

        // The type of map from
        Type sourceType;

        // The type to map to
        Type destinationType;

        // The bindings supporting the dynamic properties
        static DependencyProperty ParametersProperty = DependencyProperty.Register("Parameters", 
                                                                                   typeof(WorkflowParameterBindingCollection), 
                                                                                   typeof(MapperActivity), 
                                                                                   new PropertyMetadata(DependencyPropertyOptions.Metadata |
                                                                                                        DependencyPropertyOptions.ReadOnly));
        [BrowsableAttribute(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public WorkflowParameterBindingCollection Parameters
        {
            get { return (WorkflowParameterBindingCollection)GetValue(ParametersProperty); }
        }

        // Whether the activity is allowed to build the destination object tree
        // or whether the destination tree not being available should be an error
        private bool canCreateDestination;

        [Category(MapperConstants.BehaviorCategory)]
        [Description(MapperConstants.CanCreateInstanceDescription)]
        public bool CanCreateDestination
        {
            get { return canCreateDestination; }
            set { canCreateDestination = value; }
        }

        protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
        {
            if (!Parameters.Contains(MapperConstants.SourceObject))
                throw new InvalidOperationException(MapperConstants.NoSourceObjectError);

            if( !Parameters.Contains(MapperConstants.DestinationObject) )
                throw new InvalidOperationException(MapperConstants.NoDestinationObjectError);

            foreach (Mapping m in InternalMappings)
            {
                object source = GetSourceObject(Parameters[MapperConstants.SourceObject], m.From);
                object destination = GetDestinationObject(Parameters[MapperConstants.DestinationObject], m.To);
                CopyState(source, destination, m);
            }

            // this activity is synchronous
            return ActivityExecutionStatus.Closed;
        }

        // Walk through the object tree to find the object whose value is set by this mapping
        private object GetDestinationObject(WorkflowParameterBinding binding, string path)
        {
            // Get the destination root creating if necessary and allowed
            object destination = Parameters[MapperConstants.DestinationObject].GetBinding(WorkflowParameterBinding.ValueProperty).GetRuntimeValue(this);
            if (destination == null)
            {
                if (CanCreateDestination)
                {
                    destination = Activator.CreateInstance(destinationType);
                    Parameters[MapperConstants.DestinationObject].GetBinding(WorkflowParameterBinding.ValueProperty).SetRuntimeValue(this, destination);
                }
                else
                {
                    throw new MappingException(MapperConstants.CanCreateInstanceError);
                }
            }
            List<string> pathElements = new List<string>(path.Split(MapperConstants.PathSplitter));
            // if the mapping referes to sub-objects
            if (pathElements.Count > 1)
            {
                // walk down the path 
                while (pathElements.Count > 1)
                {
                    MemberInfo[] mis = destination.GetType().GetMember(pathElements[0]);
                    if (mis.Length == 0)
                    {
                        throw new MappingException(string.Format(MapperConstants.MemberDoesNotExistError, pathElements[0], destination.GetType().Name));
                    }
                    MemberInfo nextMember = mis[0];

                    // get the current object - if null create it
                    object temp = GetValue(destination, pathElements[0]);
                    if (temp == null)
                    {
                        if (CanCreateDestination)
                        {
                            temp = Activator.CreateInstance(GetMemberType(nextMember));
                            SetMember(destination, pathElements[0], temp);
                        }
                        else
                        {
                            throw new MappingException(MapperConstants.CanCreateInstanceError);
                        }
                    }

                    // set the current focus to the next object
                    destination = temp;

                    // remove the processed element from the list of elements
                    pathElements.RemoveAt(0);
                }
            }

            return destination;
        }

        // sets the data for a specific member of an object
        // this wraps up the fatc that the member may be a property 
        // or a field
        void SetMember(object target, string member, object val)
        {
            Type currentType = target.GetType();
            MemberInfo[] mis = currentType.GetMember(member);
            if (mis.Length == 0)
                throw new MappingException(string.Format(MapperConstants.MemberDoesNotExistError, member, currentType.Name));

            if (mis[0] is FieldInfo)
            {
                FieldInfo fi = (FieldInfo)mis[0];
                fi.SetValue(target, val);
            }
            else if (mis[0] is PropertyInfo)
            {
                PropertyInfo pi = (PropertyInfo)mis[0];
                pi.SetValue(target, val, null);
            }
            else
            {
                throw new MappingException(string.Format(MapperConstants.MemberWrongTypeError, member));
            }
        }

        // Gets the value for a member of an object
        // This wraps up the fact that a member could be
        // a property or a field
        object GetValue(object target, string member)
        {
            object ret = null;
            Type currentType = target.GetType();
            MemberInfo[] mis = currentType.GetMember(member);
            if (mis.Length == 0)
                throw new MappingException(string.Format(MapperConstants.MemberDoesNotExistError, member, currentType.Name));

            if (mis[0] is FieldInfo)
            {
                FieldInfo fi = (FieldInfo)mis[0];
                ret = fi.GetValue(target);
            }
            else if (mis[0] is PropertyInfo)
            {
                PropertyInfo pi = (PropertyInfo)mis[0];
                ret = pi.GetValue(target, null);
            }
            else
            {
                throw new MappingException(string.Format(MapperConstants.MemberWrongTypeError, member));
            }
            return ret;
        }

        // Gets the type of a member
        // this wraps up the fatc that the member may be a property 
        // or a field
        Type GetMemberType(MemberInfo mi)
        {
            Type ret = null;

            if (mi is FieldInfo)
            {
                FieldInfo fi = (FieldInfo)mi;
                ret = fi.FieldType;
            }
            else if (mi is PropertyInfo)
            {
                PropertyInfo pi = (PropertyInfo)mi;
                ret = pi.PropertyType;
            }
            else
            {
                throw new MappingException(string.Format(MapperConstants.MemberWrongTypeError, mi.Name));
            }
            return ret;
        }

        // Gets the object to take the data from by walking the path
        private object GetSourceObject(WorkflowParameterBinding binding, string p)
        {
            object root = binding.GetBinding(WorkflowParameterBinding.ValueProperty).GetRuntimeValue(this);
            object ret = root;

            List<string> pathElements = new List<string>(p.Split(MapperConstants.PathSplitter));

            while (pathElements.Count > 1)
            {
                ret = GetValue(ret, pathElements[0]);

                pathElements.RemoveAt(0);
            }

            return ret;
        }

        // Copues the data from the source to the destination based on the mapping
        private void CopyState(object Source, object Destination, Mapping m)
        {
            MemberInfo[] mis = Source.GetType().GetMember(GetLookupMember(m.From));
            if (mis.Length == 0)
                throw new MappingException(string.Format(MapperConstants.MemberDoesNotExistError, GetLookupMember(m.From), Source.GetType().Name));

            MemberInfo miSource = mis[0];

            mis = Destination.GetType().GetMember(GetLookupMember(m.To));
            if (mis.Length == 0)
                throw new MappingException(string.Format(MapperConstants.MemberDoesNotExistError, GetLookupMember(m.To), Destination.GetType().Name));

            MemberInfo miDest = mis[0];

            object sourceValue = null;
            if (miSource is PropertyInfo)
            {
                PropertyInfo pi = (PropertyInfo)miSource;
                sourceValue = pi.GetValue(Source, null);
            }
            else if (miSource is FieldInfo)
            {
                FieldInfo fi = (FieldInfo)miSource;
                sourceValue = fi.GetValue(Source);
            }
            else
            {
                throw new MappingException(string.Format(MapperConstants.MemberWrongTypeError, miSource.Name));
            }

            if (miDest is PropertyInfo)
            {
                PropertyInfo pi = (PropertyInfo)miDest;
                pi.SetValue(Destination, sourceValue, null);
            }
            else if (miDest is FieldInfo)
            {
                FieldInfo fi = (FieldInfo)miDest;
                fi.SetValue(Destination, sourceValue);
            }
            else
            {
                throw new MappingException(string.Format(MapperConstants.MemberWrongTypeError, miDest.Name));
            }

        }

        // Gets the final part of the path that is used for the data transfer
        private string GetLookupMember(string p)
        {
            string[] bits = p.Split(MapperConstants.PathSplitter);
            return bits[bits.Length - 1];
        }

        #region IMapProperties Members

        [Category(MapperConstants.BehaviorCategory)]
        [Description(MapperConstants.SourceTypeDescription)]
        [Editor(typeof(TypeBrowserEditor), typeof(UITypeEditor))]
        public Type SourceType
        {
            get
            {
                return sourceType;
            }
            set
            {
                sourceType = value;
            }
        }

        [Category(MapperConstants.BehaviorCategory)]
        [Description(MapperConstants.DestinationTypeDescription)]
        [Editor(typeof(TypeBrowserEditor), typeof(UITypeEditor))]
        public Type DestinationType
        {
            get
            {
                return destinationType;
            }
            set
            {
                destinationType = value;
            }
        }

        // This propety is the one manipulated by the editor which requires read/write 
        // access to the property. However, the workflow designer serializatiom requires
        // that the property it serializes be readonly. Therefore this property is 
        // "Hidden" from the serializer and under the covers manipulates the InternalMappings
        // readonly collection that is hidden from the property grid
        [Browsable(true)]
        [Description(MapperConstants.MappingsDescription)]
        [Category(MapperConstants.BehaviorCategory)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
        [Editor(typeof(MappingEditor), typeof(UITypeEditor))]
        public MappingCollection Mappings
        {
            get { return InternalMappings; }
            set 
            {
                InternalMappings.Clear();
                foreach (Mapping m in value)
                {
                    InternalMappings.Add(m);
                }
            }
        }

        public static DependencyProperty InternalMappingsProperty =
                            DependencyProperty.Register("InternalMappings",
                                                        typeof(MappingCollection),
                                                        typeof(MapperActivity),
                                                        new PropertyMetadata(DependencyPropertyOptions.Metadata |
                                                                             DependencyPropertyOptions.ReadOnly));
        // This property is not manipulated by the design time environment but is used for code serialization 
        // of the activity to code or XAML
        [Browsable(false)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
        public MappingCollection InternalMappings
        {
            get { return (MappingCollection)GetValue(InternalMappingsProperty); }
        }

        // Allows the editor to work out if a mapping is valid according to 
        // the source and destination types
        public bool ValidateMapping(Mapping mapping)
        {
            Type srcType = GetTargetType(SourceType, mapping.From);
            Type destType = GetTargetType(DestinationType, mapping.To);

            return srcType == destType;
        }

        #endregion

        // Helper function to get the type of the leaf part of the path
        Type GetTargetType(Type rootType, string path)
        {
            Type currentType = rootType;

            List<string> pathElements = new List<string>(path.Split(MapperConstants.PathSplitter));

            while (pathElements.Count > 0 )
            {
                MemberInfo[] mis = currentType.GetMember(pathElements[0]);
                if (mis.Length == 0)
                    throw new MappingException(string.Format(MapperConstants.MemberDoesNotExistError, pathElements[0], currentType.Name));

                currentType = GetMemberType(mis[0]);

                pathElements.RemoveAt(0);
            }

            return currentType;
        }
    }


    [Serializable]
    public class MappingException : Exception
    {
        public MappingException() { }
        public MappingException(string message) : base(message) { }
        public MappingException(string message, Exception inner) : base(message, inner) { }
        protected MappingException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }
    }
}
